-
Notifications
You must be signed in to change notification settings - Fork 309
login: Support web-based auth methods #600
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @chrisbobbe! Looking forward to having this in.
Generally the code all looks good. Comments below, mostly small.
As you mention, we'll want some tests on WebAuthPayload
. We have a few tests of that kind in zulip-mobile in webAuth-test.js
.
Let's also have a screenshot of the new login screen, and a video of the end-to-end auth flow. Those will be helpful for seeing what the UX is like.
lib/widgets/login.dart
Outdated
} | ||
|
||
class _PasswordLoginPageState extends State<PasswordLoginPage> { | ||
class LoginPageState extends State<LoginPage> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
class LoginPageState extends State<LoginPage> { | |
@visibleForTesting | |
class LoginPageState extends State<LoginPage> { |
That way it's clear this isn't meant for any other part of the app to meddle in the details of.
(And then I think the similar annotation on debugOtpOverride
becomes redundant.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
… Ah I see, this type is used by _ZulipAppState.didPushRouteInformation
, to call handleWebAuthUrl
.
Oddly when I added this @visibleForTesting
, that didn't cause any analysis error. I guess in particular that means the one on debugOtpOverride
wouldn't be redundant with it after all.
From my comment at lastBuiltKey
, though, I think this state type can remain private.
test/example_data.dart
Outdated
@@ -107,14 +122,14 @@ final User selfUser = user(fullName: 'Self User', email: 'self@example'); | |||
final Account selfAccount = account( | |||
id: 1001, | |||
user: selfUser, | |||
apiKey: 'asdfqwer', | |||
apiKey: 'dQcEJWTq3LczosDkJnRTwf31zniGvMrO', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can leave a brief note on how these are generated:
apiKey: 'dQcEJWTq3LczosDkJnRTwf31zniGvMrO', | |
// A Zulip API key is 32 digits of base64. | |
apiKey: 'dQcEJWTq3LczosDkJnRTwf31zniGvMrO', |
/// before the end of the test. | ||
// TODO(upstream) simplify callers by using addTearDown: https://github.com/flutter/flutter/issues/123189 | ||
// See also: https://github.com/flutter/flutter/issues/121917 | ||
FakeImageHttpClient prepareBoringImageHttpClient() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
test [nfc]: Move prepareBoringImageHttpClient to test_images.dart
This was already being used for tests outside the test file it was
defined in, and we'd like to start using it in an additional
different test file. So, define it centrally in this place that
makes sense.
Ah good thought, thanks. I'd forgotten there was this natural home for it already.
assets/l10n/app_en.arb
Outdated
@@ -281,6 +289,17 @@ | |||
"@loginFormSubmitLabel": { | |||
"description": "Button text to submit login credentials." | |||
}, | |||
"loginMethodDivider": "OR", | |||
"@loginMethodDivider": { | |||
"description": "Text on the divider between the username/password form and the third-party login options, like Google. Uppercase (for languages with letter case)." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: On my first reading, I thought "like Google" referred to the divider resembling something Google does.
Probably possible to reword, but I think the ", like Google" part can be left out entirely and it seems just as clear.
assets/l10n/app_en.arb
Outdated
"errorWebAuthOperationalErrorTitle": "Operational error", | ||
"@errorWebAuthOperationalErrorTitle": { | ||
"description": "Error title when third-party authentication has an operational error (not necessarily caused by invalid credentials)." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"errorWebAuthOperationalErrorTitle": "Operational error", | |
"@errorWebAuthOperationalErrorTitle": { | |
"description": "Error title when third-party authentication has an operational error (not necessarily caused by invalid credentials)." | |
"errorWebAuthOperationalErrorTitle": "Something went wrong", | |
"@errorWebAuthOperationalErrorTitle": { | |
"description": "Error title when third-party authentication has an operational error (not necessarily caused by invalid credentials)." |
Or other possibilities. But I think "operational error" is likely to be opaque jargon for most users.
lib/widgets/login.dart
Outdated
try { | ||
assert (await ZulipBinding.instance.supportsCloseForLaunchMode(_webAuthLaunchMode)); | ||
await ZulipBinding.instance.closeInAppWebView(); | ||
if ((debugOtpOverride ?? _otp) == null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This fallback between the two fields gets repeated several times, so let's pull it into a little method, like _getOtp
.
Then as a bonus, if you want to write this 100% the way the Flutter framework would: that method can use asserts so that outside of debug mode, the debug field never even gets read. (In a hot path, that sort of thing is important if you don't fully trust the optimizer to be able to prove that the field is always null. This is far from a hot path, so the optimization really doesn't matter.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a little curious how the Flutter framework does this, but in this case I'm not sure how exactly to do it. assert
only does anything in debug mode, right, so I'm not sure what change we'd want to make to affect things outside of debug mode.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An example of what I have in mind is UpdateMachine.debugEnableRegisterNotificationToken
. The key is that the assert
callback sets a local variable that belongs to the outer function.
Then for authentic upstream examples, browse through framework.dart
with that pattern in mind. The first one I spot from just starting to read through the Element
implementation is Element.debugIsDefunct
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting; thanks! I think I've got it:
String? _otp;
String? _getOtp() {
String? result = _otp;
assert(() {
result = LoginPage.debugOtpOverride ?? _otp;
return true;
}());
return result;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, exactly.
I think you can also simplify slightly, deduplicating the use of _otp
, by initializing to null
, having the assert override that, and then using ?? _otp
after that.
lib/widgets/login.dart
Outdated
onPressed: !_inProgress | ||
? () => _beginWebAuth(method) | ||
: null, | ||
label: Text(zulipLocalizations.signInWithFoo(method.displayName))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code feels a bit cramped from being indented so far to the right. I think it'd benefit from pulling out a piece as a local variable.
Specifically I think the Column
, with a name like loginForm
, would work well.
lib/widgets/login.dart
Outdated
if (e is PlatformException && e.message != null && e.message!.startsWith('Error while launching')) { | ||
// Ignore; I've seen this on my iPhone even when auth succeeds. | ||
// Specifically, Apple web auth…which on iOS should be replaced by | ||
// Apple native auth; that's #462. | ||
// Possibly related: | ||
// https://github.com/flutter/flutter/issues/91660 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, wacky.
Let's perhaps limit this condition more tightly, like only to iOS. That way if there's an error we didn't expect — which might mean the launch really did fail — we'll show the user an error message rather than silently do nothing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two other upstream reports that are directly about errors like this one:
- [url_launcher] Throw exception when clicked on Stop loading button on Safari flutter/flutter#75691
- [url_launcher] - PlatformException on ios 13.3 when opening a docx file flutter/flutter#49162
This looks like possibly the most relevant:
flutter/flutter#75691 (comment)
Does that match the situation you were seeing these errors in?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure. In that comment, the person says "if anything fails browser side". But when I watch what goes on in the browser, nothing stands out to me as matching that description.
test/widgets/login_test.dart
Outdated
pushedRoutes = []; | ||
final testNavObserver = TestNavigatorObserver() | ||
..onPushed = (route, prevRoute) => pushedRoutes.add(route); | ||
await tester.pumpWidget(ZulipApp(navigatorObservers: [testNavObserver],)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
await tester.pumpWidget(ZulipApp(navigatorObservers: [testNavObserver],)); | |
await tester.pumpWidget(ZulipApp(navigatorObservers: [testNavObserver])); |
test/widgets/login_test.dart
Outdated
|
||
// TODO test _inProgress logic? | ||
|
||
final encoded = debugEncodeApiKey(eg.selfAccount.apiKey, LoginPageState.debugOtpOverride!); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
final encoded = debugEncodeApiKey(eg.selfAccount.apiKey, LoginPageState.debugOtpOverride!); | |
final encoded = debugEncodeApiKey(eg.selfAccount.apiKey, otp); |
where otp
is a local, and you refer to debugOtpOverride
just to set it once.
That way this line doesn't get so long.
b351c0b
to
ff277e7
Compare
Hmm, a test failure in CI:
A flake, do you think? |
ff277e7
to
edf7496
Compare
Thanks for the review! Revision pushed, and I've posted a screenshot and video in the PR description. |
Hmm. Does seem unlikely to be caused by this change — so it may be a pre-existing flake. If so, then it's a bug in either the test or the code under test, so it'd be good to fix. Would you file a bug to track that flake? |
Sure: #602 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Generally this all looks great. There's one more test I'd like, and then various small style things.
I also want to try this end-to-end myself, and look closer at those settings in the manifest and Info.plist. But posting these code comments without blocking on those.
lib/api/model/web_auth.dart
Outdated
userId = int.tryParse(userIdStr, radix: 10); | ||
if (userId == null) throw const FormatException(); | ||
} | ||
|
||
final Uri? realm = Uri.tryParse(realmStr); | ||
if (realm == null) throw const FormatException(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: put these in general-to-specific order, same as in the pattern above
lib/widgets/login.dart
Outdated
const LoginPage({super.key, required this.serverSettings}); | ||
|
||
/// A key for the page from the last [buildRoute] call. | ||
static final _lastBuiltKey = GlobalKey<_LoginPageState>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's order all these members in the way suggested in the Flutter style guide:
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#order-other-class-members-in-a-way-that-makes-sense
I think that'll make it somewhat easier to look at this class and understand the API.
(It looks like the existing code wasn't quite in that order, and perhaps it'd be better if it were. But that matters a lot less when it's so simple; as it gains more complexity, the structure becomes more important for the reader.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like the existing code wasn't quite in that order
Hmm, really? Here's the class before the commit that adds to it:
class LoginPage extends StatefulWidget {
const LoginPage({super.key, required this.serverSettings});
final GetServerSettingsResult serverSettings;
static Route<void> buildRoute({required GetServerSettingsResult serverSettings}) {
return _LoginSequenceRoute(
page: LoginPage(serverSettings: serverSettings));
}
@override
State<LoginPage> createState() => _LoginPageState();
}
I see the following, matching the order in the style guide:
- Constructors […]
[…]
- Final fields that are set from the constructor.
- Other static methods [i.e. that don't return the same type as the class].
[…]
- Methods […]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see that the updated class, with web-auth things added to it, doesn't match the numbered list; I'll fix that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see the following, matching the order in the style guide:
Oh, I guess that's right. I was reading buildRoute
as returning the class's type, but it doesn't.
I think it's likely helpful to treat it for the ordering as if it does return the class's type, though. It plays pretty much the role of a factory constructor — it's the thing that application code, at least, should always use instead of calling the actual constructor directly. (Possibly we should even make the constructor private when we have a buildRoute
method like this; but that might be annoying in tests.)
lib/widgets/login.dart
Outdated
String? _otp; | ||
String? _getOtp() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is functionally a getter backed by a mutable field, so let's follow the Flutter style guide's order for those:
https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#order-other-class-members-in-a-way-that-makes-sense
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also could make this an actual getter/field pair. E.g. String? get _otp
and String? __otp;
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting; yeah, I guess it's fine to have a thing that starts with two underscores. 😛
test/api/model/web_auth_test.dart
Outdated
final wellFormed = Uri.parse( | ||
'zulip://login?otp_encrypted_api_key=$encryptedApiKey&email=self%40example&user_id=1&realm=https%3A%2F%2Fchat.example%2F'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: break line for readability
final wellFormed = Uri.parse( | |
'zulip://login?otp_encrypted_api_key=$encryptedApiKey&email=self%40example&user_id=1&realm=https%3A%2F%2Fchat.example%2F'); | |
final wellFormed = Uri.parse( | |
'zulip://login?otp_encrypted_api_key=$encryptedApiKey' | |
'&email=self%40example&user_id=1&realm=https%3A%2F%2Fchat.example%2F'); |
test/api/model/web_auth_test.dart
Outdated
}); | ||
|
||
test('parse fails when otp_encrypted_api_key is nonsense', () { | ||
final queryParams = {...wellFormed.queryParameters}..['otp_encrypted_api_key'] = 'asdf'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: key detail is off past 80 columns, so break line:
final queryParams = {...wellFormed.queryParameters}..['otp_encrypted_api_key'] = 'asdf'; | |
final queryParams = {...wellFormed.queryParameters} | |
..['otp_encrypted_api_key'] = 'asdf'; |
test/api/model/web_auth_test.dart
Outdated
check(() => WebAuthPayload.parse(input)).throws<FormatException>(); | ||
}); | ||
|
||
test('decodeApiKey fails when otp is nonsense', () { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
test('decodeApiKey fails when otp is nonsense', () { | |
test('decodeApiKey fails when otp is wrong length', () { |
Seems more precise about what's really being checked here. (And an OTP is random noise when it is properly generated, so the reader may wonder what it means for it to be "nonsense".)
test/api/model/web_auth_test.dart
Outdated
'zulip://login?otp_encrypted_api_key=$encryptedApiKey&email=self%40example&user_id=1&realm=https%3A%2F%2Fchat.example%2F'); | ||
|
||
test('basic happy case', () { | ||
final result = WebAuthPayload.parse(wellFormed); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I like the name "payload" for similar locals elsewhere in this commit — I think that'd be clearer here than "result".
} | ||
} | ||
|
||
String generateOtp() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's give this a unit test, too. For a smoke test: it returns a value (rather than throw), and the value is a hex string of the right length.
And then given that a previous draft of this function elsewhere actually had a bug where the randomness was off — it had rand.nextInt(255)
instead of 256 — let's have a test that would have caught that, so necessarily a probabilistic test.
We can follow the lead of our BackoffMachine test and say the probability of a false failure just needs to be under 1e-9. So…
- generate N = 216 of these
- take the set of bytes that ever appear (after decoding them all from hex)
- check that all 256 possible byte values do appear somewhere.
That should be plenty quick to run, I hope. And each possible byte value gets N * 32 opportunities to show up, each with probability 1/256; so its probability of missing all of those is exp(- N * 32 / 256) < 2e-12, and there are 256 such possible byte values so the probability that any of them gets missed is < 1e-9.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then since we'll be generating all those sample results, may as well use those to subsume the smoke test: just check each of them has the right length (and we'll already be decoding them as hex so implicitly checking they're hex strings).
One possible failure mode of a buggy implementation could be to drop leading zero bytes, for example, and this check would catch that.
Greg points out, referring to the Flutter style guide at https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#order-other-class-members-in-a-way-that-makes-sense : zulip#600 (comment) > I think it's likely helpful to treat [`buildRoute`] for the > ordering as if it does return the class's type, though. It plays > pretty much the role of a factory constructor — it's the thing > that application code, at least, should always use instead of > calling the actual constructor directly. (Possibly we should even > make the constructor private when we have a `buildRoute` method > like this; but that might be annoying in tests.)
Thanks for the review! Revision pushed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the revision! Small comments on the new test; otherwise all looks good.
There's also those other review steps (#600 (review)) that I want to make before merging.
test/api/model/web_auth_test.dart
Outdated
final bytesThatAppear = <int>{}; | ||
for (final otp in manyOtps) { | ||
final bytes = hex.decode(otp); | ||
check(bytes.length).equals(32); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: process more on the result of check
rather than its argument:
check(bytes.length).equals(32); | |
check(bytes).length.equals(32); |
test/api/model/web_auth_test.dart
Outdated
bytesThatAppear.addAll(bytes); | ||
} | ||
|
||
final byteValues = List.generate(256, (index) => index); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: can use Iterable.generate instead of allocating a List, and then in fact can leave out the callback argument:
final byteValues = List.generate(256, (index) => index); | |
final byteValues = Iterable.generate(256); |
Then probably clearest to just inline that.
test/api/model/web_auth_test.dart
Outdated
test('smoke, and check all 256 byte values are used', () { | ||
const n = 216; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's make the principle here a bit more explicit — helpful for anyone thinking of adding another probabilistic test and finding this as an example:
test('smoke, and check all 256 byte values are used', () { | |
const n = 216; | |
test('smoke, and check all 256 byte values are used', () { | |
// This is a probabilistic test. We've chosen `n` so that when the test | |
// should pass, the probability it fails is < 1e-9. See analysis below. | |
const n = 216; |
group('generateOtp', () { | ||
test('smoke, and check all 256 byte values are used', () { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did a quick test to confirm this test runs quickly, and it does. Running this test file with or without this test case included, 3 runs each, the difference in runtime is less than the noise between runs:
$ time flutter test test/api/model/web_auth_test.dart
00:09 +8: All tests passed!
time: 10.660s wall (13.258s u, 0.864s s)
$ time flutter test test/api/model/web_auth_test.dart
00:01 +8: All tests passed!
time: 2.808s wall (3.231s u, 0.613s s)
$ time flutter test test/api/model/web_auth_test.dart
00:01 +8: All tests passed!
time: 2.724s wall (3.155s u, 0.596s s)
$ time flutter test test/api/model/web_auth_test.dart
00:01 +8: All tests passed!
time: 2.702s wall (3.165s u, 0.581s s)
$ time flutter test test/api/model/web_auth_test.dart --name WebA
00:01 +7: All tests passed!
time: 2.764s wall (3.275s u, 0.580s s)
$ time flutter test test/api/model/web_auth_test.dart --name WebA
00:01 +7: All tests passed!
time: 2.759s wall (3.110s u, 0.642s s)
$ time flutter test test/api/model/web_auth_test.dart --name WebA
00:01 +7: All tests passed!
time: 2.710s wall (3.188s u, 0.568s s)
(I made one warmup run before the runs I counted, though I was surprised how big the warmup effect was.)
In particular I'm pretty sure the runtime is <50ms, which is fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
Hmm, well — this doesn't work for me, when I try it on my device. I tried rust-lang.zulipchat.com, and hit the GitHub button. In the UI, I got the "Something went wrong" dialog box. In logcat, there was this:
That "component name for" message appears to come from
If I just comment out our @@ -321,9 +321,9 @@ class _LoginPageState extends State<LoginPage> {
try {
final url = widget.serverSettings.realmUrl.resolve(method.loginUrl)
.replace(queryParameters: {'mobile_flow_otp': _otp!});
- if (!(await ZulipBinding.instance.canLaunchUrl(url))) {
- throw Error();
- }
+ // if (!(await ZulipBinding.instance.canLaunchUrl(url))) {
+ // throw Error();
+ // } then…
And meanwhile in logcat there's this:
Then I commented out that other check: - assert (await ZulipBinding.instance.supportsCloseForLaunchMode(_webAuthLaunchMode));
+ // assert (await ZulipBinding.instance.supportsCloseForLaunchMode(_webAuthLaunchMode)); and tried again, and it works! |
So in short it does work if we remove those two checks. Both checks seem to be trying to confirm that the thing the code is about to do next is expected to work. So it's probably best to just try the thing, and if it doesn't work then that's an error we have to deal with. |
This also probably indicates it's a good idea to make a minimal effort to report the actual error message when there's an error, because it's likely there will be errors some users run into and we'll want that information for debugging. In particular if |
<meta-data android:name="flutter_deeplinking_enabled" android:value="true" /> | ||
<intent-filter> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, the changes here and in Info.plist LGTM.
(The intent filter and CFBundleUrlTypes
match what we have in zulip-mobile; the other bits are what's prescribed in the docs under https://docs.flutter.dev/ui/navigation/deep-linking .)
So what remains to do on this PR is those small comments at #600 (review) on the new test, and then the bigger point from my end-to-end testing after that.
Hmm, I'm seeing that |
Thanks for the review! Revision pushed, and see my comment just before this: #600 (comment) |
Ah, yeah, I read too quickly: abstract interface class Exception {
factory Exception([var message]) => _Exception(message);
} So it's an interface, but with no members — just a marker interface. And its "message" is only an argument to the factory constructor. Anyway, we can do so for |
We're about to add some more login options.
To sit alongside the existing launchUrl.
The binding classes are getting a bit crowded with all the stuff from url_launcher; perhaps we can pull it all out to a helper as a followup.
And also go a bit further than what the test was doing: simulate a tap on the error dialog's dismiss button.
We're about to add the web-auth feature, and the tests for that feature will want their own separate group.
Soon we'll reuse this setup to test the upcoming web-auth feature, and that feature exercises functionality on _ZulipAppState.
This was already being used for tests outside the test file it was defined in, and we'd like to start using it in an additional different test file. So, define it centrally in this place that makes sense.
Greg points out, referring to the Flutter style guide at https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#order-other-class-members-in-a-way-that-makes-sense : zulip#600 (comment) > I think it's likely helpful to treat [`buildRoute`] for the > ordering as if it does return the class's type, though. It plays > pretty much the role of a factory constructor — it's the thing > that application code, at least, should always use instead of > calling the actual constructor directly. (Possibly we should even > make the constructor private when we have a `buildRoute` method > like this; but that might be annoying in tests.)
The top of this area will never be in the top inset, since that's consumed by the app bar. So "minimum 8" really just means always 8. So, express that more directly and transparently.
OK, retested and this version works out of the box. The changes look good, so I'll go ahead and merge. Glad to have this feature in! Then I'll file a follow-up issue (→ #609) for putting more information in the error dialog, for the sake of debugging whatever errors people turn out to run into. Probably be good to do that soon after, while it's fresh in mind (and also so we have it soon for that debugging.) |
RPReplay_Final1711671338.mov
Fixes: #36